Skip to content

fix(identity): reconcile managedBy:'better-auth' apiMethods with the write guard (#1591)#3213

Merged
os-zhuang merged 2 commits into
mainfrom
claude/continue-task-1591-yqg8hb
Jul 18, 2026
Merged

fix(identity): reconcile managedBy:'better-auth' apiMethods with the write guard (#1591)#3213
os-zhuang merged 2 commits into
mainfrom
claude/continue-task-1591-yqg8hb

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Continues #1591 — lands Phase 1 + Phase 2 of the development plan in this comment. The headline vulnerability (generic /data CRUD bypassing better-auth) was already closed by the engine-level identity write guard (ADR-0092 D2). What remained was the metadata contradiction: identity objects still advertised generic write verbs in enable.apiMethods that the guard rejects anyway.

Phase 1 — tighten apiMethods on better-auth-managed objects

For every managedBy: 'better-auth' object in packages/platform-objects/src/identity/, the generic write verbs (create/update/delete) were removed from enable.apiMethods, leaving reads (get/list) only. Effect: the HTTP exposure gate (ADR-0049, enforceApiAccess) now answers a semantically-correct 405 before the engine's 403 backstop even runs, and the schema metadata matches its own doc comments.

The one exception is sys_user, which keeps update — the single generic write opened on an identity table (ADR-0092 D4), server-side clamped to the profile-field whitelist ({name, image}) by the write guard and declared in-schema via userActions: { edit: true }.

Objects tightened: sys_account, sys_api_key, sys_device_code, sys_invitation, sys_member, sys_organization, sys_session, sys_team, sys_team_member, sys_two_factor, sys_user (reads + update), sys_verification. Objects already read-only or API-disabled (sys_oauth_*, sys_scim_provider, sys_sso_provider, sys_jwks) were left as-is.

Phase 2 — registration-time consistency backstop

New reconcileManagedApiMethods runs inside SchemaRegistry.registerObject. For a managedBy: 'better-auth' object it strips any generic write verb the object does not grant — computed from resolveCrudAffordances (the managedBy bucket default plus userActions overrides, the same function the UI uses) — logging a warning. Reads are never touched; non-better-auth objects are a no-op. This makes the contradiction impossible to reintroduce: even if a schema re-adds delete, the effective exposed set is corrected at registration.

Generalizing this to other managedBy buckets via an externallyManaged/writeVia capability (Phase 3) stays with #1878 / ADR-0049 and is intentionally out of scope here.

Verification

  • Registry unit tests (packages/objectql/src/registry.test.ts) — new reconcileManagedApiMethods suite: strips create/update/delete with no affordances; keeps update when userActions.edit grants edit (the sys_user case); no-op when nothing needs stripping; never touches reads; leaves platform-bucket objects untouched; and applies through registerObject. Full suite: 69 passed.
  • Dogfood (single-tenant-identity-create.dogfood.test.ts) — updated to assert POST /data/sys_team now returns 405 OBJECT_API_METHOD_NOT_ALLOWED (was 403 PERMISSION_DENIED); system-context insert still succeeds. Ran green against the booted showcase stack.
  • @objectstack/platform-objects (213) and @objectstack/plugin-auth identity-write-guard + auth-plugin (75) suites pass; builds/DTS clean.

Closes #1591.

🤖 Generated with Claude Code


Generated by Claude Code

…write guard (#1591)

Phase 1 — tighten `apiMethods` on better-auth-managed identity objects.
`managedBy: 'better-auth'` promises generic CRUD is suppressed (writes flow
through better-auth's endpoints; the plugin-auth identity write guard rejects
direct user-context create/update/delete), yet these schemas still advertised
the write verbs in `enable.apiMethods`. That made the HTTP exposure gate admit
the request and let it 403 at the engine instead of answering a clean 405, and
the metadata contradicted its own doc comments. Each better-auth object is now
read-only (`get`/`list`), except `sys_user`, which keeps `update` — the one
generic write opened on an identity table (ADR-0092 D4), server-side clamped to
the profile-field whitelist ({name, image}) and declared via `userActions.edit`.

Phase 2 — registration-time consistency backstop. `reconcileManagedApiMethods`
runs in `SchemaRegistry.registerObject` and strips any generic write verb a
better-auth object advertises but does not grant (per `resolveCrudAffordances`:
the managedBy bucket default plus `userActions` overrides), with a warning.
Reads are never touched; non-better-auth objects are untouched. This makes the
contradiction impossible to reintroduce — even if a schema re-adds a write verb,
the effective exposed set is corrected at registration. Generalizing to other
`managedBy` buckets (`externallyManaged`/`writeVia`) stays with #1878.

Proven: registry unit tests for the derivation; the identity-create dogfood
suite updated to assert `POST /data/sys_team` now returns 405
(OBJECT_API_METHOD_NOT_ALLOWED) at the exposure gate, before the engine's 403
backstop — system-context inserts still succeed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GwY68hKVysP98BX7i5eUoE
@vercel

vercel Bot commented Jul 18, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
spec Canceled Canceled Jul 18, 2026 1:22pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/objectql, @objectstack/platform-objects, packages/qa.

16 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/objectql)
  • content/docs/data-modeling/formulas.mdx (via packages/objectql)
  • content/docs/deployment/migration-from-objectql.mdx (via @objectstack/objectql)
  • content/docs/deployment/vercel.mdx (via @objectstack/objectql)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/objectql)
  • content/docs/kernel/services.mdx (via @objectstack/objectql)
  • content/docs/permissions/authentication.mdx (via @objectstack/objectql)
  • content/docs/permissions/authorization.mdx (via packages/qa)
  • content/docs/permissions/delegated-administration.mdx (via packages/qa)
  • content/docs/plugins/index.mdx (via @objectstack/objectql)
  • content/docs/plugins/packages.mdx (via @objectstack/objectql, @objectstack/platform-objects)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/objectql)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/objectql)
  • content/docs/releases/implementation-status.mdx (via @objectstack/objectql)
  • content/docs/releases/v9.mdx (via @objectstack/objectql)
  • content/docs/ui/setup-app.mdx (via @objectstack/platform-objects)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

Comment thread packages/objectql/src/registry.test.ts Fixed
@os-zhuang
os-zhuang marked this pull request as ready for review July 18, 2026 13:48
@os-zhuang
os-zhuang merged commit 865ccb9 into main Jul 18, 2026
17 checks passed
@os-zhuang
os-zhuang deleted the claude/continue-task-1591-yqg8hb branch July 18, 2026 13:48
os-zhuang added a commit that referenced this pull request Jul 18, 2026
…sys_metadata (#3220) (#3222)

Follow-through on #1591/#3213 for two non-better-auth managed objects that
shipped the same contradiction the better-auth reconciliation fixed: their
enable.apiMethods advertised generic create/update/delete while their managedBy
bucket forbids user-context writes, leaving the generic /data route open.

- sys_presence (append-only) advertised create/update/delete — update/delete on
  an append-only object — but is written only over the realtime websocket/
  in-memory path, never through ObjectQL. Narrowed to ['get','list'].
- sys_metadata (system) advertised full CRUD but overlays are authored only via
  the metadata-protocol RPC (engine writes carry a transaction context, not a
  user session); neither the framework nor the Console (objectui) POSTs
  /data/sys_metadata. Narrowed to ['get','list'].

Reads stay open. The metadata-protocol / realtime write paths are engine-level
and bypass the HTTP exposure gate, so they are unaffected — verified by the
metadata-authoring dogfood (5 passed), the objectql overlay engine-insert tests
(6 passed), and metadata-core (100 passed).

A blast-radius audit found the broader system/append-only buckets are NOT safe
to guard wholesale: several system objects (sys_user_position,
sys_user_permission_set, sys_position_permission_set, sys_user_preference,
sys_import_job) are user-writable by design (delegated administration, user
preferences, imports). Generalizing the engine write guard to those buckets is
intentionally out of scope — the root cause is the overloaded system bucket,
tracked in #3220.


Claude-Session: https://claude.ai/code/session_01GwY68hKVysP98BX7i5eUoE

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

managedBy is not enforced: generic CRUD bypasses better-auth on sys_team (data-integrity / security)

3 participants